summaryrefslogtreecommitdiff
path: root/app/[lng]
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]')
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/page.tsx325
1 files changed, 325 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx
new file mode 100644
index 00000000..27936560
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx
@@ -0,0 +1,325 @@
+// app/rfq/page.tsx
+
+import * as React from "react";
+import { Metadata } from "next";
+import { type SearchParams } from "@/types/table";
+import { Shell } from "@/components/shell";
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton";
+import { HelpCircle, Package, FileText, ClipboardList, Layers } from "lucide-react";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { RfqTable } from "@/lib/rfq-last/table/rfq-table";
+import { getRfqs } from "@/lib/rfq-last/service";
+import { searchParamsRfqCache } from "@/lib/rfq-last/validations";
+import { InformationButton } from "@/components/information/information-button";
+
+export const metadata: Metadata = {
+ title: "RFQ 관리",
+ description: "RFQ 견적 요청 관리 시스템",
+};
+
+interface RfqPageProps {
+ searchParams: Promise<SearchParams>;
+}
+
+// 프로세스 안내 팝오버 컴포넌트
+function ProcessGuidePopover() {
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button variant="ghost" size="icon" className="h-6 w-6">
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-96" align="start">
+ <div className="space-y-3">
+ <div className="space-y-1">
+ <h4 className="font-medium">RFQ 프로세스</h4>
+ <p className="text-sm text-muted-foreground">
+ 견적 요청 관리 프로세스입니다.
+ </p>
+ </div>
+
+ <div className="space-y-3 text-sm">
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 1
+ </div>
+ <div>
+ <p className="font-medium">RFQ 생성</p>
+ <p className="text-muted-foreground">ECC 또는 수동으로 RFQ를 생성합니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 2
+ </div>
+ <div>
+ <p className="font-medium">구매담당 지정</p>
+ <p className="text-muted-foreground">각 RFQ에 구매 담당자를 지정합니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 3
+ </div>
+ <div>
+ <p className="font-medium">업체 선정 및 발송</p>
+ <p className="text-muted-foreground">Short List를 확정하고 RFQ를 발송합니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 4
+ </div>
+ <div>
+ <p className="font-medium">견적 접수</p>
+ <p className="text-muted-foreground">업체로부터 견적서를 접수받습니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 5
+ </div>
+ <div>
+ <p className="font-medium">최종 업체 선정</p>
+ <p className="text-muted-foreground">TBE를 통해 최종 업체를 선정합니다.</p>
+ </div>
+ </div>
+ </div>
+
+ <div className="border-t pt-3 space-y-2">
+ <h5 className="font-medium text-sm">RFQ 유형</h5>
+ <div className="space-y-2 text-xs">
+ <div className="flex items-center gap-2">
+ <FileText className="h-3 w-3 text-blue-600" />
+ <span className="font-medium">일반견적:</span>
+ <span className="text-muted-foreground">일반 견적 요청</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <Package className="h-3 w-3 text-purple-600" />
+ <span className="font-medium">ITB:</span>
+ <span className="text-muted-foreground">프로젝트 기반 입찰</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <ClipboardList className="h-3 w-3 text-green-600" />
+ <span className="font-medium">RFQ:</span>
+ <span className="text-muted-foreground">PR 기반 견적 요청</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
+ );
+}
+
+// 탭별 데이터 카운트를 가져오는 함수
+async function getTabCounts() {
+ try {
+ const [allData, generalData, itbData, rfqData] = await Promise.all([
+ getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "general" }),
+ getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "itb" }),
+ getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "rfq" }),
+ ]);
+
+ return {
+ general: generalData.total || 0,
+ itb: itbData.total || 0,
+ rfq: rfqData?.total || 0,
+ };
+ } catch (error) {
+ console.error("Error fetching tab counts:", error);
+ return {
+ all: 0,
+ general: 0,
+ itb: 0,
+ rfq: 0,
+ };
+ }
+}
+
+export default async function RfqPage(props: RfqPageProps) {
+ const searchParams = await props.searchParams;
+
+ // nuqs 기반 파라미터 파싱
+ const search = searchParamsRfqCache.parse(searchParams);
+
+ // 탭별 데이터 카운트 가져오기
+ const tabCounts = await getTabCounts();
+
+ // 현재 선택된 탭 (URL 파라미터에서 가져오거나 기본값 'all')
+ const currentTab = search.rfqCategory || "all";
+
+ // 각 탭별로 데이터 프리패칭
+// const allData = await getRfqs({ ...search, rfqCategory: "all" });
+ const generalData = await getRfqs({ ...search, rfqCategory: "general" });
+ const itbData = await getRfqs({ ...search, rfqCategory: "itb" });
+ const rfqData = await getRfqs({ ...search, rfqCategory: "rfq" });
+
+ return (
+ <Shell className="gap-4">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ RFQ 관리
+ </h2>
+ <InformationButton pagePath="rfq" />
+ <ProcessGuidePopover />
+ </div>
+ </div>
+
+ {/* 탭 컨테이너 */}
+ <Tabs defaultValue={currentTab} className="w-full">
+ <TabsList className="grid w-full max-w-[600px] grid-cols-4">
+
+ <TabsTrigger value="itb" className="relative">
+ <Package className="mr-2 h-4 w-4" />
+ ITB
+ {tabCounts.itb > 0 && (
+ <Badge variant="secondary" className="ml-2 text-xs">
+ {tabCounts.itb}
+ </Badge>
+ )}
+ </TabsTrigger>
+ <TabsTrigger value="rfq" className="relative">
+ <ClipboardList className="mr-2 h-4 w-4" />
+ RFQ
+ {tabCounts.rfq > 0 && (
+ <Badge variant="secondary" className="ml-2 text-xs">
+ {tabCounts.rfq}
+ </Badge>
+ )}
+ </TabsTrigger>
+ <TabsTrigger value="general" className="relative">
+ <FileText className="mr-2 h-4 w-4" />
+ 일반견적
+ {tabCounts.general > 0 && (
+ <Badge variant="secondary" className="ml-2 text-xs">
+ {tabCounts.general}
+ </Badge>
+ )}
+ </TabsTrigger>
+ </TabsList>
+
+
+ {/* 일반견적 탭 */}
+ <TabsContent value="general" className="mt-4">
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={13}
+ searchableColumnCount={4}
+ filterableColumnCount={8}
+ cellWidths={[
+ "3rem", // checkbox
+ "9rem", // rfqCode
+ "7rem", // status
+ "8rem", // rfqType
+ "15rem", // rfqTitle
+ "8rem", // projectCode
+ "12rem", // projectName
+ "8rem", // picName
+ "5rem", // rfqSendDate
+ "5rem", // dueDate
+ "5rem", // vendorCount
+ "5rem", // quotationReceived
+ "5rem", // actions
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <RfqTable
+ data={generalData}
+ rfqCategory="general"
+ />
+ </React.Suspense>
+ </TabsContent>
+
+ {/* ITB 탭 */}
+ <TabsContent value="itb" className="mt-4">
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={14}
+ searchableColumnCount={4}
+ filterableColumnCount={9}
+ cellWidths={[
+ "3rem", // checkbox
+ "9rem", // rfqCode
+ "7rem", // status
+ "10rem", // projectCompany
+ "8rem", // projectFlag
+ "10rem", // projectSite
+ "6rem", // smCode
+ "8rem", // projectCode
+ "12rem", // projectName
+ "8rem", // picName
+ "5rem", // rfqSendDate
+ "5rem", // dueDate
+ "5rem", // vendorCount
+ "5rem", // actions
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <RfqTable
+ data={itbData}
+ rfqCategory="itb"
+ />
+ </React.Suspense>
+ </TabsContent>
+
+ {/* RFQ(PR) 탭 */}
+ <TabsContent value="rfq" className="mt-4">
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={15}
+ searchableColumnCount={4}
+ filterableColumnCount={10}
+ cellWidths={[
+ "3rem", // checkbox
+ "9rem", // rfqCode
+ "7rem", // status
+ "8rem", // prNumber
+ "8rem", // prIssueDate
+ "8rem", // series
+ "8rem", // projectCode
+ "12rem", // projectName
+ "8rem", // itemCode
+ "12rem", // itemName
+ "8rem", // picName
+ "5rem", // rfqSendDate
+ "5rem", // dueDate
+ "5rem", // vendorCount
+ "5rem", // actions
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <RfqTable
+ data={rfqData}
+ rfqCategory="rfq"
+ />
+ </React.Suspense>
+ </TabsContent>
+ </Tabs>
+ </Shell>
+ );
+} \ No newline at end of file